﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MyAlbum.Model;
using System.Xml.Linq;
using System.Threading;
using MyAlbum.Utility;


namespace MyAlbum.Data.Xml
{
    public class TagEntryRepository : XmlRepository, ITagEntryRepository
    {
        //<Tag Id=""><Name>tagName</Name>3<Count></Count></Tag>
        private static object locker = "locker";
        public TagEntryRepository(string rootPath)
            : base(rootPath, "Tags")
        {
        }

        #region ITagEntryRepository Members

        /// <summary>
        /// Returns all tags
        /// </summary>
        /// <returns></returns>
        public List<MyAlbum.Model.Tag> GetTags()
        {
            return this.Document.Descendants("Tag").Select(s => new MyAlbum.Model.Tag()
            {
                Id = (s.Attribute("Id").Value),
                Name = s.Element("Name").Value.Trim(),
                Count = (int)s.Element("Count")
            }).ToList();
        }

        /// <summary>
        /// Load the tags for the specified entry id
        /// </summary>
        /// <param name="entryId">The entry id</param>
        /// <returns></returns>
        internal IEnumerable<MyAlbum.Model.Tag> GetTagsForEntry(string entryId)
        {
            TagEntryRelationRepository relationsRepository = new TagEntryRelationRepository(base._rootPath);
            return from r in relationsRepository.Document.Descendants("Relation")
                   join t in this.Document.Descendants("Tag") on
                   (r.Attribute("TagId").Value) equals
                   (t.Attribute("Id").Value)
                   where r.Attribute("EntryId").Value == entryId
                   select new MyAlbum.Model.Tag()
                   {
                       Id = (t.Attribute("Id").Value),
                       Name = t.Element("Name").Value.Trim(),
                       Count = (int)t.Element("Count")
                   };
        }


        /// <summary>
        /// Delete a tag (usually when no entries were found for it,
        /// meaning it's a 'dead' tag) from the tags and tags_entry tables
        /// </summary>
        /// <param name="tagName">The tag name to look for</param>
        public void DeleteTagName(string tagName)
        {
           lock(locker)
            {
                TagEntryRelationRepository relationsRepository = new TagEntryRelationRepository(base._rootPath);
                var tags = this.Document.Descendants("Tag")
                    .Where(s => s.Element("Name").Value.Trim() == tagName).ToList();
                for (int i = 0; i < tags.Count; i++)
                {
                    var item = tags[i];
                    string id = item.Attribute("Id").Value;
                    relationsRepository.DeleteRelation(id);
                    item.Remove();
                }
                this.Save();
            }
        }

        #endregion

        /// <summary>
        /// Load a tag by name
        /// </summary>
        /// <param name="tagName">The tag name</param>
        /// <returns>Tag if found, null if not</returns>
        internal Tag LoadTag(string tagName)
        {
            XElement tag = this.Document.Descendants("Tag")
                .Where(s => s.Element("Name").Value.Trim() == tagName)
                .SingleOrDefault();


            if (tag != null)
            {
                return new MyAlbum.Model.Tag()
                {
                    Id = (tag.Attribute("Id").Value),
                    Name = tagName,
                    Count = (int)tag.Element("Count")
                };
            }
            else
            {
                return null;
            }

        }

        /// <summary>
        /// Create a new tag and return the new XElement created for the tag
        /// </summary>
        /// <param name="tag">Tag data</param>
        /// <returns>XElement with the tag data</returns>
        /// <exception cref="ArgumentNullException"></exception>
        private XElement AddTagInternal(Tag tag)
        {
            Assert.IsNotNull(tag);
            Assert.IsNotNull(tag.Name);
            tag.Id = Guid.NewGuid().ToString();
            XElement a = new XElement("Tag",
                new XAttribute("Id", tag.Id),
                 new XElement("Name", tag.Name),
            new XElement("Count", tag.Count));
            Document.Element("Tags").Add(a);
            this.Save();
            return a;
        }

        /// <summary>
        /// Create a new tag and add relation
        /// </summary>
        /// <param name="entryId">The entry id</param>
        /// <param name="tagName">The tag name</param>
        internal void AddTag(string entryId, string tagName)
        {
            lock (locker)
            {
                //load the tag element
                XElement tagElement = LoadTagInternal(tagName);

                //if not found create it
                if (tagElement == null)
                {
                    Tag t = new Tag();
                    t.Name = tagName.Trim();
                    t.Count = 0;
                    tagElement = AddTagInternal(t);
                }
                //increase the count
                int curCount = Convert.ToInt32(tagElement.Element("Count").Value);
                tagElement.Element("Count").ReplaceWith(new XElement("Count", ++curCount));
                this.Save();

                //check if we have relation, if not create it
                TagEntryRelationRepository relationsRepository = new TagEntryRelationRepository(base._rootPath);
                relationsRepository.AddRelationIfNotExists((tagElement.Attribute("Id").Value), entryId);
            }
        }

        /// <summary>
        /// Load the tag's XElement
        /// </summary>
        /// <param name="tagName">The name of the tag</param>
        /// <returns>XElement if found, null if not</returns>
        private XElement LoadTagInternal(string tagName)
        {
            return this.Document.Descendants("Tag")
                 .Where(s => s.Element("Name").Value.Trim() == tagName)
                 .SingleOrDefault();
        }

        /// <summary>
        /// Load the entries id with the specified tag name
        /// </summary>
        /// <param name="tagName">The tag name</param>
        /// <returns>List of IDs</returns>
        internal List<string> GetEntriesIdsForTagName(string tagName)
        {
            TagEntryRelationRepository relationsRepository = new TagEntryRelationRepository(base._rootPath);
            return (from r in relationsRepository.Document.Descendants("Relation")
                     join
                     t in this.Document.Descendants("Tag")
                      on (r.Attribute("TagId").Value)
                     equals (t.Attribute("Id").Value)
                     where t.Element("Name").Value.Trim() == tagName
                     select (r.Attribute("EntryId").Value)).ToList();

        }

        /// <summary>
        /// Delete tags and relations for the spcified entry
        /// </summary>
        /// <param name="entryId">The entry id</param>
        internal void RemoveTagsForEntry(string entryId)
        {
            lock (locker)
            {
                TagEntryRelationRepository relationsRepository = new TagEntryRelationRepository(base._rootPath);

                //load all relations for the entry
                List<XElement> relations = relationsRepository.Document.Descendants("Relation").Where(s => (s.Attribute("EntryId").Value) == entryId).ToList();

                //for each relation update the tags and delete the relation
                for (int i = 0; i < relations.Count; i++)
                {
                    var item = relations[i];
                    //load the tag element
                    XElement tagElement = LoadTagInternal((item.Attribute("TagId").Value));

                    //if found update count or delete
                    if (tagElement != null)
                    {
                        int curCount = Convert.ToInt32(tagElement.Element("Count").Value);

                        if (curCount > 1)
                        {
                            //we have other relations for this tag
                            curCount--;
                            tagElement.Element("Count").ReplaceWith(new XElement("Count", curCount));
                        }
                        else
                        {
                            //this is the only relation, delete it
                            tagElement.Remove();
                        }

                        //delete the relation
                        item.Remove();
                    }
                }
                this.Save();
                relationsRepository.Save();
            }
        }

        /// <summary>
        /// Load the XElement for the specified tag id
        /// </summary>
        /// <param name="tagId">The tag id</param>
        /// <returns>XElement if found, null if not</returns>
        private XElement LoadTagInternal(Guid tagId)
        {
            return this.Document.Descendants("Tag")
                .Where(s => new Guid(s.Attribute("Id").Value) == tagId)
                .SingleOrDefault();
        }

        /// <summary>
        /// An internal class used for managing the relations between tags and entries
        /// </summary>
        private class TagEntryRelationRepository : XmlRepository
        {
            //<Relation TagId="" EntryId=""/>
            public TagEntryRelationRepository(string rootPath)
                : base(rootPath, "TagEntryRelations")
            {
            }

            /// <summary>
            /// Delete a relations for a tag
            /// </summary>
            /// <param name="tagId">The tag id</param>
            internal void DeleteRelation(string tagId)
            {
                lock (locker)
                {
                    var relations = this.Document.Descendants("Relation").Where(r => (r.Attribute("TagId").Value) == tagId).ToList();
                    for (int i = 0; i < relations.Count; i++)
                    {
                        relations[i].Remove();
                    }
                    this.Save();
                }
            }

            /// <summary>
            /// Add a relation between a tag and an entry
            ///  if one does not exist already
            /// </summary>
            /// <param name="tagId">The tag id</param>
            /// <param name="entryId">The entry id</param>
            internal void AddRelationIfNotExists(string tagId, string entryId)
            {
                lock (locker)
                {
                    if (this.Document.Descendants("Relation")
                        .FirstOrDefault(s => (s.Attribute("TagId").Value) == tagId
                            && (s.Attribute("EntryId").Value) == entryId) == null)
                    {
                        this.Document.Element("TagEntryRelations")
                            .Add(new XElement("Relation",
                                new XAttribute("TagId", tagId),
                                new XAttribute("EntryId", entryId)));
                        this.Save();
                    }
                }

            }
        }

    }
}
